package net.aiion.weave.impl.messaging;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

import net.aiion.weave.spi.bytecode.Bytecode;
import net.aiion.weave.spi.messaging.Message;
import net.aiion.weave.spi.messaging.MessageCallback;
import net.aiion.weave.spi.messaging.MessageException;
import net.aiion.weave.spi.messaging.MessageQueue;
import net.aiion.weave.spi.messaging.MessageReceiver;
import net.aiion.weave.spi.messaging.MessageReceiverContext;
import net.aiion.weave.spi.messaging.annotations.Recipient;
import net.aiion.weave.spi.naming.Context;
import net.aiion.weave.spi.naming.annotations.DynamicScoped;
import net.aiion.weave.spi.naming.annotations.NotScoped;

public class MessageQueueImpl implements MessageQueue {
    private final Context namingContext;
    private final String queueName;

    public MessageQueueImpl(final Context aNamingContext, final String aQueueName) {
        namingContext = aNamingContext;
        queueName = aQueueName;
    }

    public String getName() {
        return queueName;
    }

    public void sendMessage(final Object aMessage) {
        sendMessage(aMessage, null);
    }

    @Override
    public void sendMessage(final Object aMessage, final MessageCallback aMessageCallback) {
        if (aMessage == null) {
            return;
        }

        final Collection<Context> targetContexts = findTargetContexts(aMessage);

        final String recipient = getRecipientFor(aMessage);
        if (!recipient.isEmpty()) {
            for (Context targetContext : targetContexts) {
                dispatchMessageTo(targetContext, aMessage, aMessageCallback, recipient);
            }
        } else {
            for (Context targetContext : targetContexts) {
                dispatchMessage(targetContext, aMessage, aMessageCallback);
            }
        }
    }

    private String getRecipientFor(final Object aMessage) {
        final Class<?> messageClass = aMessage.getClass();
        if (messageClass.isAnnotationPresent(Recipient.class)) {
            return messageClass.getAnnotation(Recipient.class).value();
        }

        if (aMessage instanceof Message) {
            return ((Message) aMessage).getRecipient();
        }

        return "";
    }

    private Collection<Context> findTargetContexts(final Object aMessage) {
        Collection<Context> targetContexts = findTargetContextsUp(namingContext, aMessage);
        if (targetContexts.isEmpty()) {
            targetContexts = findTargetContextsDown(namingContext, aMessage);
        }

        if (targetContexts.isEmpty()) {
            // If there is no @Scope annotation on the Message's class, we assume, that the message should only be
            // dispatched in the current context and deeper. This behavior is equivalent to the behavior, as if the
            // Message's class would be annotated with on of the @NotScoped or @DynamicScoped annotations.
            targetContexts = Collections.singleton(namingContext);
        }

        return targetContexts;
    }

    private Collection<Context> findTargetContextsDown(final Context aBaseNamingContext, final Object aMessage) {
        final Collection<Context> targetNamingContexts = new ArrayList<>();
        for (Context childNamingContext : aBaseNamingContext.getChildContexts()) {
            final Collection<Context> childTargetNamingContexts = findTargetContextsDown(childNamingContext, aMessage);
            if (!childTargetNamingContexts.isEmpty()) {
                targetNamingContexts.addAll(childTargetNamingContexts);
            }
        }
        return targetNamingContexts;
    }

    private Collection<Context> findTargetContextsUp(final Context aBaseNamingContext, final Object aMessage) {
        final Class<?> messageClass = aMessage.getClass();
        if (messageClass.isAnnotationPresent(aBaseNamingContext.getScope())
            || messageClass.isAnnotationPresent(NotScoped.class)
            || messageClass.isAnnotationPresent(DynamicScoped.class)) {
            return Collections.singleton(aBaseNamingContext);
        }

        if (aBaseNamingContext.getParentContext() != null) {
            return findTargetContextsUp(aBaseNamingContext.getParentContext(), aMessage);
        }

        return Collections.emptyList();
    }

    private void dispatchMessageTo(final Context aContext, final Object aMessage,
        final MessageCallback aMessageCallback, final String aMessageReceiverName) {
        final Object messageReceiver = aContext.lookup(aMessageReceiverName);
        if (messageReceiver == null) {
            for (Context childContext : aContext.getChildContexts()) {
                dispatchMessageTo(childContext, aMessage, aMessageCallback, aMessageReceiverName);
            }
        }

        if (messageReceiver instanceof MessageReceiver) {
            dispatchMessageTo(aContext, aMessage, aMessageCallback, (MessageReceiver) messageReceiver);
        }
    }

    private void dispatchMessageTo(final Context aContext, final Object aMessage,
        final MessageCallback aMessageCallback, final MessageReceiver aMessageReceiver) {
        deliverMessage(aMessage, aMessageCallback, aMessageReceiver);
    }

    private void dispatchMessage(final Context aContext, final Object aMessage, final MessageCallback aMessageCallback) {
        final Collection<Class<? extends MessageReceiver>> messageReceiverClasses = Bytecode
            .findClassesByInterfaceClass(MessageReceiver.class);
        for (Class<? extends MessageReceiver> messageReceiverClass : messageReceiverClasses) {
            dispatchMessage(aContext, aMessage, aMessageCallback, messageReceiverClass);
        }
    }

    private void dispatchMessage(final Context aContext, final Object aMessage, final MessageCallback aMessageCallback,
        final Class<? extends MessageReceiver> aMessageReceiverClass) {
        final MessageReceiver messageReceiver = aContext.lookup(aMessageReceiverClass);
        if (messageReceiver == null) {
            for (Context childContext : aContext.getChildContexts()) {
                dispatchMessage(childContext, aMessage, aMessageCallback, aMessageReceiverClass);
            }
        } else {
            deliverMessage(aMessage, aMessageCallback, messageReceiver);
        }
    }

    private void deliverMessage(final Object aMessage, final MessageCallback aMessageCallback,
        final MessageReceiver aMessageReceiver) {
        try {
            final MessageReceiverContext messageReceiverContext = new MessageReceiverContextImpl(queueName, aMessage);
            aMessageReceiver.receiveMessage(messageReceiverContext);
            if (aMessageCallback != null) {
                aMessageCallback.onComplete(new MessageResultImpl(aMessageReceiver, aMessage, messageReceiverContext
                    .getResult()));
            }
        } catch (final MessageException e) {
            if (aMessageCallback != null) {
                aMessageCallback.onComplete(new MessageResultImpl(aMessageReceiver, aMessage, e));
            } else {
                throw e;
            }
        }
    }
}
